#!/usr/bin/perl -w
##############################################################################
# Copyright (c) 2008, League of Crafty Programmers Ltd <info@locp.co.uk>
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY LEAGUE OF CRAFTY PROGRAMMERS ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL LEAGUE OF CRAFTY PROGRAMMERS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
##############################################################################
use Getopt::Long;
use IO::Handle;
use Nmap::Parser;
use Pod::Usage;

# Find hosts that are in both the observed and baseline scans.  Compare the
# status of the host and the status of each of the individual ports.
sub show_changed_hosts {
	my ($format, $baseline, $observed) = @_;
	$output = "\nChanged hosts:\n" if ($format eq 'text');
	$output = "" if ($format eq 'xml');

	for my $baseline_ip ($baseline->get_ips()) {
		my $observed_host = $observed->get_host($baseline_ip);
		next if (!defined($observed_host));

		# To have reached here the host exists in both the baseline
		# and the observed scans.
		my $host = Host->new();
		$host->hostname($observed_host->hostname());
		$host->ip_addr($baseline_ip);
		$host->status($observed_host->status());
		my $host_has_changed = 0;
		my @changed_services = ();
		my @missing_services = ();
		my @new_services = ();
		my $baseline_host = $baseline->get_host($baseline_ip);
		$host_has_changed = 1
		    if ($observed_host->status() ne $baseline_host->status());

		# Check for new or changed TCP services
		my @observed_services = $observed_host->tcp_ports();
		my @baseline_services = $baseline_host->tcp_ports();

		for my $portid (@observed_services) {
			my $s = $observed_host->tcp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("tcp");
			$service->status(
			    $observed_host->tcp_port_state($portid));

			if (!grep {$_ eq $portid} @baseline_services) {
				$host_has_changed = 1;
				push @new_services, $service;
				next;
			}

			my $service2 = Service->new();
			$service2->name($s->name());
			$service2->portid($portid);
			$service2->proto("tcp");
			$service2->status(
			    $baseline_host->tcp_port_state($portid));

			if ($service->compare($service2) != 0) {
				$host_has_changed = 1;
				push @changed_services, $service;
			}
		}

		# Check for missing TCP ports

		for my $portid (@baseline_services) {
			my $s = $baseline_host->tcp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("tcp");
			$service->status(
			    $baseline_host->tcp_port_state($portid));

			if (!grep {$_ eq $portid} @observed_services) {
				$host_has_changed = 1;
				push @missing_services, $service;
				next;
			}
		}

		# Check for new or changed UDP services
		@observed_services = $observed_host->udp_ports();
		@baseline_services = $baseline_host->udp_ports();

		for my $portid (@observed_services) {
			my $s = $observed_host->udp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("udp");
			$service->status(
			    $observed_host->udp_port_state($portid));

			if (!grep {$_ eq $portid} @baseline_services) {
				$host_has_changed = 1;
				push @new_services, $service;
				next;
			}

			my $service2 = Service->new();
			$service2->name($s->name());
			$service2->portid($portid);
			$service2->proto("udp");
			$service2->status(
			    $baseline_host->udp_port_state($portid));

			if ($service->compare($service2) != 0) {
				$host_has_changed = 1;
				push @changed_services, $service;
			}
		}

		# Check for missing UDP ports

		for my $portid (@baseline_services) {
			my $s = $baseline_host->udp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("udp");
			$service->status(
			    $baseline_host->udp_port_state($portid));

			if (!grep {$_ eq $portid} @observed_services) {
				$host_has_changed = 1;
				push @missing_services, $service;
				next;
			}
		}

		next if (!$host_has_changed);

		# To have reached here then a change has been detected in the
		# host between the scans.

		if ($format eq 'text') {
			$output .= "\t" . $host->to_string() . "\n";
			$output .= "\t\tNew Services:\n";

			for my $service (@new_services) {
				$output .= "\t\t\t" . $service->to_string()
				    . "\n";
			}

			$output .= "\t\tMissing Services:\n";

			for my $service (@missing_services) {
				$output .= "\t\t\t" . $service->to_string()
				    . "\n";
			}

			$output .= "\t\tChanged Services:\n";

			for my $service (@changed_services) {
				$output .= "\t\t\t" . $service->to_string()
				    . "\n";
			}

		} else {
			# XML output for differing hosts.
			$output .= "  " . $host->to_xml(0) . "\n";

			if (scalar(@new_services) == 0) {
				$output .= "   <new_services/>\n";
			} else {
				$output .= "   <new_services>\n";

				for my $service (@new_services) {
					$output .= "    "
					    . $service->to_xml()
					    . "\n";
				}

				$output .= "   </new_services>\n";
			}

			if (scalar(@missing_services) == 0) {
				$output .= "   <missing_services/>\n";
			} else {
				$output .= "   <missing_services>\n";

				for my $service (@missing_services) {
					$output .= "    "
					    . $service->to_xml()
					    . "\n";
				}

				$output .= "   </missing_services>\n";
			}

			if (scalar(@changed_services) == 0) {
				$output .= "   <changed_services/>\n";
			} else {
				$output .= "   <changed_services>\n";

				for my $service (@changed_services) {
					$output .= "    "
					    . $service->to_xml()
					    . "\n";
				}

				$output .= "   </changed_services>\n";
			}

			$output .= "  </host>\n";
		}
	}

	if ($format eq 'xml') {
		if ($output) {
			$output = " <changed>\n" . $output . " </changed>\n";
		} else {
			$output = " <changed/>\n";
		}
	}

	print $output;
}

# Find hosts that are defined in the baseline but are missing from the
# observed scan.
sub show_missing_hosts {
	my ($format, $baseline, $observed) = @_;
	my @hosts = ();

	for my $baseline_ip ($baseline->get_ips()) {
		my $observed_host = $observed->get_host($baseline_ip);

		if (!defined($observed_host)) {
			my $baseline_host = $baseline->get_host($baseline_ip);
			my $host = Host->new();
			$host->hostname($baseline_host->hostname());
			$host->ip_addr($baseline_ip);
			push @hosts, $host;
		}
	}

	my $output = '';
	$output = "\nMissing hosts:\n" if ($format eq 'text');
	$output = " <missing>\n" if (scalar(@hosts) != 0 && $format eq 'xml');
	$output = " <missing/>\n" if (scalar(@hosts) == 0 && $format eq 'xml');

	for my $host (@hosts) {
		$output .= "\t" . $host->to_string . "\n"
		    if ($format eq 'text');
		$output .= "  " . $host->to_xml(1) . "\n"
		    if ($format eq 'xml');
	}

	$output .= " </missing>\n"
	    if (scalar(@hosts) != 0 && $format eq 'xml');
	print $output;
}

# Find hosts that are defined in the baseline scan, but are missing from the
# observed scan.
sub show_new_hosts {
	my ($format, $baseline, $observed) = @_;
	my @hosts = ();

	for my $observed_ip ($observed->get_ips()) {
		my $baseline_host = $baseline->get_host($observed_ip);

		if (!defined($baseline_host)) {
			my $observed_host = $observed->get_host($observed_ip);
			my $host = Host->new();
			$host->hostname($observed_host->hostname());
			$host->ip_addr($observed_ip);
			$host->status($observed_host->status());

			for my $portid ($observed_host->tcp_ports()) {
				my $s = $observed_host->tcp_service($portid);
				my $service = Service->new();
				$service->name($s->name());
				$service->portid($portid);
				$service->proto("tcp");
				$service->status(
				    $observed_host->tcp_port_state($portid));
				$host->add_service($service);
			}

			for my $portid ($observed_host->udp_ports()) {
				my $s = $observed_host->udp_service($portid);
				my $service = Service->new();
				$service->name($s->name());
				$service->portid($portid);
				$service->proto("udp");
				$service->status(
				    $observed_host->udp_port_state($portid));
				$host->add_service($service);
			}

			push @hosts, $host;
		}
	}

	my $output = '';
	$output = "\nNew hosts:\n" if ($format eq 'text');
	$output = " <new>\n" if (scalar(@hosts) != 0 && $format eq 'xml');
	$output = " <new/>\n" if (scalar(@hosts) == 0 && $format eq 'xml');

	for my $host (@hosts) {
		if ($format eq 'text') {
			$output .= "\t" . $host->to_string . "\n";
		} else {
			$output .= "  " . $host->to_xml(0) . "\n";
		}

		for my $portid ($host->get_tcp_service_list()) {
			my $svc = $host->get_service($portid, "tcp");

			if ($format eq 'text') {
				$output .= "\t\t" . $svc->to_string() . "\n";
			} else {
				$output .= "   " . $svc->to_xml() . "\n";
			}
		}

		for my $portid ($host->get_udp_service_list()) {
			my $svc = $host->get_service($portid, "udp");

			if ($format eq 'text') {
				$output .= "\t\t" . $svc->to_string() . "\n";
			} else {
				$output .= "   " . $svc->to_xml() . "\n";
			}
		}

		$output .= "  </host>\n" if ($format eq 'xml');
	}

	$output .= " </new>\n"
	    if (scalar(@hosts) != 0 && $format eq 'xml');
	print $output;
}

##############################################################################
# Main processing starts here.
##############################################################################

# Declare and initialise variables.
my $baseline_file = '';
my $command_line = '';
my $format = 'text';
my $gen_stylesheet = '';
my $help = 0;
my $man = 0;
my $observed_file = '';
my $output_file = '';
my $output_hosts = '';
my $release = 2;
my $run_date = `date`;
my $show_version = 0;
my $show_changed_hosts = 1;
my $show_missing_hosts = 1;
my $show_new_hosts = 1;
my $stylesheet = '';
my $version = 0;

##############################################################################
# Command line parsing.
##############################################################################

for $entry (@ARGV) {
	$command_line .= $entry . ' ';
}

chomp $command_line;
GetOptions(
    'baseline=s' => \$baseline_file,
    'format=s' => \$format,
    'gen-stylesheet=s' => \$gen_stylesheet,
    'help|?' => \$help,
    'man' => \$man,
    'observed=s' => \$observed_file,
    'output-file=s' => \$output_file,
    'output-hosts=s' => \$output_hosts,
    'stylesheet=s' => \$stylesheet,
    'version' => \$show_version
    ) or pod2usage(1);

pod2usage(1) if $help;
pod2usage(-verbose => 2) if $man;

if ($show_version) {
	print "Version: $version.$release\n";
	exit(0);
}

if ($gen_stylesheet) {
	my $xslt = new Stylesheet;
	$xslt->generate($gen_stylesheet);
	exit(0);
}

if ($output_hosts) {
	$show_changed_hosts = $show_missing_hosts = $show_new_hosts = 0;

	for (my $i = 0; $i < length($output_hosts); $i++) {
		$c = substr($output_hosts, $i, 1);
		if ($c eq "c") {
			$show_changed_hosts = 1;
		} elsif ($c eq "m") {
			$show_missing_hosts = 1;
		} elsif ($c eq "n") {
			$show_new_hosts = 1;
		} else {
			pod2usage(0);
		}
	}
}

if ($format eq 'text' || $format eq 'xml') {
	;
} else {
	pod2usage(0);
}

##############################################################################
# Open and parse the files
##############################################################################
pod2usage(0) if !$baseline_file;
my $baseline = new Nmap::Parser;
$baseline->parsefile($baseline_file);
pod2usage(0) if !$observed_file;
my $observed = new Nmap::Parser;
$observed->parsefile($observed_file);

# Redirect the output if requested.  Obviously this must be called after
# all command line checks and usage messages.
if ($output_file) {
	open OUTPUT, '>', $output_file or die $!;
	STDOUT->fdopen(\*OUTPUT, 'w') or dir $!;
}

# Do the report.

if ($format eq 'text') {
	print 'xndiff run ', $run_date,
	    'command line: ', $command_line, "\n",
	    'baseline: ', $baseline_file, "\n",
	    'observed: ', $observed_file, "\n";
} else {
	print '<?xml version="1.0" encoding="utf-8"?>',
	    "\n";
	print '<?xml-stylesheet type="text/xsl" href="',
	    $stylesheet,
	    '"?>',
	    "\n" if ($stylesheet);
	chomp $run_date;
	print "<xndiff rundate=\"$run_date\"",
	    ' version="', $version, '.', $release, '"',
	    ' command_line="', $command_line, '">', "\n";
	print " <parameters>\n";
        my $session = $baseline->get_session();
	print "  <baseline>\n",
            "   <file>$baseline_file</file>\n",
            '   <scan_args>', $session->scan_args, "</scan_args>\n",
            '   <nmap_version>', $session->nmap_version, "</nmap_version>\n",
            '   <scan_start>', $session->start_str, "</scan_start>\n",
	    "  </baseline>\n";
        $session = $observed->get_session();
	print "  <observed>\n",
            "   <file>$baseline_file</file>\n",
            '   <scan_args>', $session->scan_args, "</scan_args>\n",
            '   <nmap_version>', $session->nmap_version, "</nmap_version>\n",
            '   <scan_start>', $session->start_str, "</scan_start>\n",
	    "  </observed>\n";
	print " </parameters>\n";
}

show_new_hosts($format, $baseline, $observed) if $show_new_hosts;
show_missing_hosts($format, $baseline, $observed) if $show_missing_hosts;
show_changed_hosts($format, $baseline, $observed) if $show_changed_hosts;
print "</xndiff>\n" if ($format eq 'xml');

##############################################################################
# Package: Host
#
# This package is for use with xndiff and is for using an object representing
# a host.
#
# Example:
#
#    $host = Host->new();
#    $host->hostname("example");
#    $host->status("up");
#    $host->ip_addr("192.168.0.1");
#
#    ...
#
#    $host->add_service($service);
##############################################################################
package Host;
use strict;

# Create a new instance of the Host object.
sub new {
	my $self = {};
	$self->{HOSTNAME} = undef;
	$self->{IP_ADDR} = undef;
	$self->{STATUS} = undef;
	$self->{SERVICES} = [];
	bless($self);
	return $self;
}

# Associate a Service object with this host.
sub add_service {
	my $self = shift;
	push @{ $self->{SERVICES} }, shift;
}

# A conveniance function for numeric sorting.
sub by_number_ascending
{
	$a <=> $b;
}

# Return a service that matches the associated portid and protocol.
#
# Example:
#
#    $service = $host->get_service(80, "tcp");
sub get_service {
	my $self = shift;
	my $portid = shift;
	my $proto = shift;
	my $null_service = ();

	for my $service (@{ $self->{SERVICES} }) {
		if ($portid eq $service->portid && $proto eq $service->proto) {
			return $service;
		}
	}

	return $null_service;
}

# Returns a sorted list of the portid's for the TCP services associated with
# the host.
sub get_tcp_service_list {
	my $self = shift;
	my @list = ();

	for my $service (@{ $self->{SERVICES} }) {
		push @list, $service->portid if ($service->proto eq "tcp");
	}

	return sort(by_number_ascending @list);
}

# Returns a sorted list of the portid's for the UDP services associated with
# the host.
sub get_udp_service_list {
	my $self = shift;
	my @list = ();

	for my $service (@{ $self->{SERVICES} }) {
		push @list, $service->portid if ($service->proto eq "udp");
	}

	return sort(by_number_ascending @list);
}

# Get or set the hostname
sub hostname {
	my $self = shift;
	$self->{HOSTNAME} = shift if (@_);
	return $self->{HOSTNAME};
}

# Get or set the IP address.
sub ip_addr {
	my $self = shift;
	$self->{IP_ADDR} = shift if (@_);
	return $self->{IP_ADDR};
}

# Get or set the status of the host.
sub status {
	my $self = shift;
	$self->{STATUS} = shift if (@_);
	return $self->{STATUS};
}

# Return the host details (not any associated ports) as a string.
sub to_string {
	my $self = shift;
	my $s = $self->{IP_ADDR} . ' (' . $self->{HOSTNAME} . ')';
	$s .= ' - ' . $self->{STATUS} if defined $self->{STATUS};
	return $s;
}

# Return the host details (not any associated ports) as xml.
sub to_xml {
	my $self = shift;
	my $close_fully = shift;
	my $s = "<host ip_addr=\"" . $self->{IP_ADDR}
	    . "\" hostname=\"" . $self->{HOSTNAME} . "\"";
	$s .= ' status="' . $self->{STATUS} . '"' if defined $self->{STATUS};
	my $close = ($close_fully) ? '/>' : '>';
	$s .= $close;
	return $s;
}
1;

##############################################################################
# Package: Service
#
# This package is for use with xndiff and is for using an object representing
# a TCP or UDP service.
#
# Example:
#
#    $service = Service->new();
#    $service->name("ssh");
#    $service->portid(22);
#    $service->proto("tcp");
#    $service->status("open");
#    $service->compare($another_service);
##############################################################################
package Service;
use strict;

# Create a new instance of the Service object.
sub new {
	my $self = {};
	$self->{NAME} = '';
	$self->{PORTID} = undef;
	$self->{PROTO} = undef;
	$self->{STATUS} = '';
	bless($self);
	return $self;
}

# Compare the current service with another Service instance.  Returns zero if
# the services are identical, non-zero otherwise.
sub compare {
	my $self = shift;
	my $service = shift;


	if (defined ($service->name)) {
		return 1 if (!defined ($self->{NAME}));
		return 1 if ($service->name ne $self->{NAME});
	} elsif (defined ($self->{NAME})) {
		return 1;
	}

	return 1 if ($service->status ne $self->{STATUS});
	return 0;
}

# Get or set the name of the service.
sub name {
	my $self = shift;
	$self->{NAME} = shift if (@_);
	return $self->{NAME};
}

# Get or set the portid of the service.
sub portid {
	my $self = shift;
	$self->{PORTID} = shift if (@_);
	return $self->{PORTID};
}

# Get or set the protocol of the service ("tcp" or "udp").
sub proto {
	my $self = shift;
	$self->{PROTO} = shift if (@_);
	return $self->{PROTO};
}

# Get or set the status of the service.
sub status {
	my $self = shift;
	$self->{STATUS} = shift if (@_);
	return $self->{STATUS};
}

# Return a string representation of the service.
sub to_string {
	my $self = shift;
	my $s = "$self->{PORTID}/";
	$s .= "$self->{PROTO}/";
	$s .= "$self->{STATUS}";
	$s .= " ($self->{NAME})" if ($self->{NAME});
	return $s;
}

# Return an XML representation of the service.
sub to_xml {
	my $self = shift;
	my $s = '<service portid="' . $self->{PORTID} . '"'
	    . ' proto="' . $self->{PROTO} . '"'
	    . ' status="' . $self->{STATUS} . '"';
	$s .= ' name="' . $self->{NAME} . '"' if ($self->{NAME});
	$s .= '/>';
	return $s;
}

1;

##############################################################################
# Package: Stylesheet
#
# This package is for use with xndiff and is for generating an XML
# stylesheet.
#
# Example:
#
#    $xslt = Service->new();
#    $xslt->generate(filename);
##############################################################################
package Stylesheet;
use strict;

# Create a new instance of the Stylesheet object.
sub new {
	my $self = {};
	$self->{FILENAME} = '';
	my $xsl_version = "1.1a";
	$self->{XSL} = '<?xml version="1.0" encoding="utf-8"?>
<!--
 Generated by the xndiff program.

 See http://code.google.com/p/xndiff for details.
-->

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="xndiff">
<html>
<body>
 <h1>Xndiff</h1>

 <h2>Run Parameters</h2>

 <table border="1">
  <caption><b>Xndiff Information</b></caption>
  <tr>
   <td>Run Date</td>
   <td><xsl:value-of select="@rundate"/></td>
  </tr>
  <tr>
   <td>Version</td>
   <td><xsl:value-of select="@version"/></td>
  </tr>
  <tr>
   <td>Command Arguments</td>
   <td><xsl:value-of select="@command_line"/></td>
  </tr>
 </table>

 <table border="1">
  <caption><b>Baseline Information</b></caption>
  <tr>
   <td>Run Date</td>
   <td><xsl:value-of select="parameters/baseline/scan_start"/></td>
  </tr>
  <tr>
   <td>Nmap Version</td>
   <td><xsl:value-of select="parameters/baseline/nmap_version"/></td>
  </tr>
  <tr>
   <td>Scan Arguments</td>
   <td><xsl:value-of select="parameters/baseline/scan_args"/></td>
  </tr>
 </table>

 <table border="1">
  <caption><b>Observed Information</b></caption>
  <tr>
   <td>Run Date</td>
   <td><xsl:value-of select="parameters/observed/scan_start"/></td>
  </tr>
  <tr>
   <td>Nmap Version</td>
   <td><xsl:value-of select="parameters/observed/nmap_version"/></td>
  </tr>
  <tr>
   <td>Scan Arguments</td>
   <td><xsl:value-of select="parameters/observed/scan_args"/></td>
  </tr>
 </table>

 <hr/>

 <xsl:if test="new">
  <h2>New</h2>
   <ul>

  <xsl:for-each select="new/host">
   <li><b><xsl:value-of select="@ip_addr"/>/<xsl:value-of select="@hostname"/>
   (<xsl:value-of select="@status"/>)</b>
   <table border="1">
    <tr>
     <th>Port</th>
     <th>Protocol</th>
     <th>Status</th>
     <th>Name</th>
    </tr>

    <xsl:for-each select="service">
     <tr>
      <td><xsl:value-of select="@portid"/></td>
      <td><xsl:value-of select="@proto"/></td>
      <td><xsl:value-of select="@status"/></td>
      <td><xsl:value-of select="@name"/></td>
     </tr>
    
    </xsl:for-each>

   </table></li>
  </xsl:for-each>

  </ul><hr/>
 </xsl:if>

 <xsl:if test="missing">
  <h2>Missing</h2>
  <ul>

  <xsl:for-each select="missing/host">
   <li><b><xsl:value-of select="@ip_addr"/>/<xsl:value-of select="@hostname"/>
   </b></li>
  </xsl:for-each>
  </ul><hr/>
 </xsl:if>

 <xsl:if test="changed">
  <h2>Changed</h2>
  <ul>

  <xsl:for-each select="changed/host">
   <li><b><xsl:value-of select="@ip_addr"/>/<xsl:value-of select="@hostname"/>
   (<xsl:value-of select="@status"/>)</b></li>

   <table border="1">
    <tr>
     <th>Change Type</th>
     <th>Port</th>
     <th>Protocol</th>
     <th>Status</th>
     <th>Name</th>
    </tr>

    <xsl:for-each select="new_services/service">
     <tr>
      <td>New</td>
      <td><xsl:value-of select="@portid"/></td>
      <td><xsl:value-of select="@proto"/></td>
      <td><xsl:value-of select="@status"/></td>
      <td><xsl:value-of select="@name"/></td>
     </tr>
    </xsl:for-each>

    <xsl:for-each select="missing_services/service">
     <tr>
      <td>Missing</td>
      <td><xsl:value-of select="@portid"/></td>
      <td><xsl:value-of select="@proto"/></td>
      <td><xsl:value-of select="@status"/></td>
      <td><xsl:value-of select="@name"/></td>
     </tr>
    </xsl:for-each>

    <xsl:for-each select="changed_services/service">
     <tr>
      <td>Changed</td>
      <td><xsl:value-of select="@portid"/></td>
      <td><xsl:value-of select="@proto"/></td>
      <td><xsl:value-of select="@status"/></td>
      <td><xsl:value-of select="@name"/></td>
     </tr>
    </xsl:for-each>

    </table>
  </xsl:for-each>
  </ul><hr/>
 </xsl:if>

 <sub>Ndiff stylesheet version: ' . $xsl_version . '</sub>
</body>
</html>
</xsl:template>
</xsl:stylesheet>';
	bless($self);
	return $self;
}

# Write the stylesheet to the specified filename.
sub generate {
	my $self = shift;
	my $filename = shift;
	open OUTPUT, '>', $filename or die $!;
	print OUTPUT $self->{XSL}, "\n";
	close(OUTPUT);
}
1;
__END__

=head1 NAME

xndiff - find differences between two nmap network scans

=head1 SYNOPSIS

xndiff
[--format <text | xml>]
[--output-file file]
[--output-hosts <nmc>]
[--stylesheet <file | URL>]
--baseline file
--observed file

xndiff --gen-stylesheet file

xndiff --help | -?

xndiff --man

xndiff --version

=head1 OPTIONS

=over 8

=item B<--baseline> file

Specifies the nmap results to use as the baseline for the comparison.

=item B<--format> <text | xml>

Specifies the output format (text is the default).

=item B<--gen-stylesheet> file

Generate an XML stylesheet (XSLT) suitable for use with the --stylesheet
option.

=item B<--help>

Print a brief help message and exit.

=item B<--man>

Print the manual page and exit.

=item B<--observed> file

Specifies the nmap results to use as the "observed results" for the comparison.

=item B<--output-file> file

Send the output to the specified file.

=item B<--output-hosts> <nmc>

Specifies which types of hosts to display.  Any combination of [nmc] may be
specified, as follows:

n = new hosts in the "observed" scan.

m = missing hosts in the "observed" scan.

c = changed hosts in the "observed" scan.

The default is to show new, missing and changed hosts.

=item B<--stylesheet> <file | URL>

Specifies the location of an XML stylesheet to be referred to in any XML
output.  A suitable stylesheet can be generated using the --gen-stylesheet
option.

=item B<--version>

Print the version and exit.

=back

=head1 DESCRIPTION

B<xndiff> allows a network administrator or other interested party to easily
monitor one or more networks for changes in port states and running
services.  It achieves this by comparing the results of two nmap scans,
one designated the "baseline", the other "observation".

Both baseline and observation are stored in files generated via nmap's
-oX switch for XML output.

=head1 EXAMPLES

To generate a report to screen:

=over 8

xndiff --baseline baseline.xml --observed observed.xml

=back

To generate a stylesheet to file.

=over 8

xndiff --gen-stylesheet xndiff.xsl

=back

To generate a report to an XML file and using the stylesheet generated in the
previous example.

=over 8

xndiff --baseline baseline.xml --observed observed.xml \
    --format xml --output-file report.xml

=back

=head1 HISTORY

B<xndiff> is a complete rewrite of ndiff written by James Levine, except
where ndiff used nmap's "grepable" output, xndiff reads XML output.

=head1 AUTHORS

The Xndiff project at http://code.google.com/p/xndiff

=head1 BUGS

The project members graciously accept that there may be bugs.  If there are
any found, please report them at the link below or browse the issues reported
to see if a fix is either available or in progress.

http://code.google.com/p/xndiff/issues

=cut
